﻿// Copyright (c) 2010 Dan Popick

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using System.Web;
using System.Web.Caching;

using Open.Web.Stijl.Css;
using Open.Web.Stijl.Configuration;
using Open.Web.Stijl.Utility;

namespace Open.Web.Stijl
{
	/// <summary>
	/// An HttpHandler for CSS files which automatically rewrites the sprite rules ont the fly
	/// </summary>
	/// <remarks>
	/// This will not handle situations where the CSS file changes well w/r/t users who already have
	/// the images cached. Ideally the images should always have the same cache expiration as the CSS, 
	/// but there's no real guarantee 
	/// </remarks>
    public class SpritingCssHandler: IHttpHandler
    {
		Utility.Tracer<SpritingCssHandler> _log = new Utility.Tracer<SpritingCssHandler>();
	
		class CssPair : CachedResourceInfo
        {
			public string Key { get; set; }
            public List<TiledImage> Images { get; set; }
            public byte[] Css { get; set; }
            public EncodingType Encoding { get; set; }
        }

        static ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
        static StijlConfiguration _config;

        static SpritingCssHandler()
        {
            _config = StijlConfiguration.Default;
        }

        public bool IsReusable
        {
            get { return true; }
        }

		/// <summary>
		/// Processes an HTTP request.
		/// </summary>
		/// <param name="context">context for the request</param>
        public void ProcessRequest(HttpContext context)
        {
			Stopwatch sw = new Stopwatch();
			sw.Start();

            EncodingType encoding = DetermineEncoding(context);
            try
            {
				// TODO: this is caching the file AND the encoding, so the results
				// will be regenerated for anyone requesting a different encoding. Not
				// so good -- the raw data should be cached independently of the encoding
				// and attached to the cached item as alternative encodings.
                string key = GetKey(context.Request.Url.AbsolutePath, encoding);
                CssPair pair = context.Cache[key] as CssPair;

                if (pair == null)
                {
					_log.Info("No existing pair found for the specified encoding and path ({0}, {1})", context.Request.Url.AbsoluteUri, encoding);

                    List<string> files;
                    pair = GeneratePair(context.Request.PhysicalPath, encoding, context, out files);
					pair.Key = key;
                    AggregateCacheDependency dependencies = new AggregateCacheDependency();

                    files.ForEach(x => {
						dependencies.Add(new CacheDependency(x));
						_log.Debug("{0}: {1} is a dependency", context.Request.Url.AbsolutePath, x);
					});

                    context.Cache.Insert(key, pair, dependencies);
                }

                int number;
                if (Int32.TryParse(context.Request.QueryString["n"], out number))
                    Respond(pair, number, context);
                else
                    Respond(pair, context);
            }
            catch (Exception ex)
            {
				_log.Error(ex, "An exception was while generating a response for {0}", context.Request.RawUrl);
				context.Response.StatusCode = 500;
				context.Response.ContentType = "text/plain";

				if (_config.ShowExceptionOutput)
					context.Response.Write(ex.ToString());
				else
					context.Response.Write("An internal error occurred during the processing of the request.");
            }

			_log.Debug("Handled request for {0} in {1:#,##0}ms", context.Request.RawUrl, sw.ElapsedMilliseconds);
        }

        string GetKey(string fileName, EncodingType encoding)
        {
            return String.Format("_stijl_{0}_{1}", fileName, encoding);
        }

        void Respond(CssPair pair, HttpContext context)
        {
			_log.Debug("Responding to {0} with the content generated for {1}", context.Request.RawUrl, pair.Key);

            context.Response.ContentType = "text/css";
            if (pair.Encoding != EncodingType.None)
                context.Response.AppendHeader("Content-Encoding", pair.Encoding.ToString());
            context.Response.AppendHeader("Last-Modified", pair.LastModified);
            context.Response.Cache.SetETag(pair.ETag);
            context.Response.Cache.SetCacheability(HttpCacheability.Public);
            if (_config.MaxAge > 0)
                context.Response.Cache.SetMaxAge(new TimeSpan(0, 0, _config.MaxAge));

            CachedResourceInfo cri = CachedResourceInfo.FromRequest(context.Request);
            if (!cri.IsNull
				&& cri.Equals(pair)
                && !_config.DebugMode
                && !String.Equals(context.Request.Headers["Pragma"], "no-cache", StringComparison.OrdinalIgnoreCase)
                && !String.Equals(context.Request.Headers["Cache-Control"], "no-cache", StringComparison.OrdinalIgnoreCase))
            {
				_log.Debug("...returning 304 in response to headers.");
				context.Response.StatusCode = 304;
            }
            else
            {
				_log.Debug("...returning the CSS contents.");
				context.Response.StatusCode = 200;
                context.Response.OutputStream.Write(pair.Css, 0, pair.Css.Length);
            }
        }

        void Respond(CssPair pair, int imageNumber, HttpContext context)
        {
			_log.Debug("Responding to {0} with the content generated for {1}", context.Request.RawUrl, pair.Key);

			if (imageNumber < 0 || imageNumber >= pair.Images.Count)
            {
				_log.Debug("...invalid image number");
				context.Response.StatusCode = 404;
            }
            else
            {
                CachedResourceInfo cri = CachedResourceInfo.FromRequest(context.Request);
                TiledImage im = pair.Images[imageNumber];

                context.Response.ContentType = "image/png";

                context.Response.AppendHeader("Last-Modified", im.LastModified);
                context.Response.Cache.SetETag(im.ETag);
                context.Response.Cache.SetCacheability(HttpCacheability.Public);
                if (_config.MaxAge > 0)
                    context.Response.Cache.SetMaxAge(new TimeSpan(0, 0, _config.MaxAge));

                if (!cri.IsNull
					&& cri.Equals(im)
                    && !_config.DebugMode
                    && !String.Equals(context.Request.Headers["Pragma"], "no-cache", StringComparison.OrdinalIgnoreCase)
                    && !String.Equals(context.Request.Headers["Cache-Control"], "no-cache", StringComparison.OrdinalIgnoreCase))
                {
					_log.Debug("...returning 304 in response to headers.");
					context.Response.StatusCode = 304;
                }
                else
                {
					_log.Debug("...returning the image contents.");
					context.Response.OutputStream.Write(im.ImageData, 0, im.ImageData.Length);
                    context.Response.ContentType = "image/png";
                } 
            }
        }

        /// <summary>
        /// Generates the spitesheets for a single CSS files 
        /// </summary>
        /// <param name="path">file to load</param>
        /// <param name="encoding">the output response encoding</param>
        /// <param name="context">the http context in which we're operating</param>
        /// <param name="dependentFiles">a list of files on which this generation depends (used for cache dependencies)</param>
        /// <returns>A tiled, encoded output set</returns>
        CssPair GeneratePair(string path, EncodingType encoding, HttpContext context, out List<string> dependentFiles)
        {
            string dir = Path.GetDirectoryName(path);
            string fileName = Path.GetFileName(path);
            string miniCss;

            List<TiledImage> images;
			HandlerTileEngine engine = new HandlerTileEngine();

            engine.ProcessFile(path, out miniCss, out images, out dependentFiles);

			// find oldest date for the set
			DateTime maxDate = dependentFiles.Max(x => (new FileInfo(x)).LastWriteTimeUtc);

			// encode and compute the CSS CRC
            uint crc;
            byte[] encodedCss = miniCss.Encode(encoding, out crc);

			_log.Info("Generated pair: {0}, {1}, etag: {2:x}, youngest: {3}",
				path, encoding, crc, maxDate);

            return new CssPair()
            {
                Css = encodedCss,
                Images = images,
                Encoding = encoding,
                LastModified = maxDate.ToString("r"),
                ETag = String.Format("\"{0:x}\"", crc)
            };
        }

        // determines the encoding (compression) to use for the response
        EncodingType DetermineEncoding(HttpContext context)
        {
            if (_config.DebugMode)
                return EncodingType.None; 

            string encodingTypes = context.Request.Headers["Accept-Encoding"];
            if (encodingTypes == null)
                return EncodingType.None;

            string[] parts = encodingTypes.Split(',');
            if (parts.Contains("deflate", StringComparer.OrdinalIgnoreCase))
                return EncodingType.deflate;
            if (parts.Contains("gzip", StringComparer.OrdinalIgnoreCase))
                return EncodingType.gzip;

            return EncodingType.None;
        }

        // act on the encoding value -- compress the output response
        byte[] Encode(EncodingType encoding, string data, out uint crc32)
        {
            byte[] asciiData = Encoding.ASCII.GetBytes(data);

            Open.Web.Stijl.Utility.Crc32 crc = new Utility.Crc32();
            crc.ComputeHash(asciiData);
            crc32 = crc.CrcValue;

            if (EncodingType.deflate == (encoding & EncodingType.deflate))
            {
                MemoryStream outputString = new MemoryStream();
                DeflateStream ds = new DeflateStream(outputString, CompressionMode.Compress);
                ds.Write(asciiData, 0, asciiData.Length);
                ds.Close();
                return outputString.ToArray();
            }

            if (EncodingType.gzip == (encoding & EncodingType.gzip))
            {
                MemoryStream outputString = new MemoryStream();
                GZipStream ds = new GZipStream(outputString, CompressionMode.Compress);
                ds.Write(asciiData, 0, asciiData.Length);
                ds.Close();
                return outputString.ToArray();
            }

            return asciiData;
        }
    }
}
